<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title></title>
    <link href="lib/qedit.css" rel="stylesheet"/>
    <script id="code" type="text/unknown">
function TextView(textModel){
    this.$initEventable();
    this.$initRenderable();

    this.m_textModel = textModel;
    this.$on(this.m_textModel, 'lineschanged', this._onTextModelLinesChanged);
    this.m_selection = new Selection(this);
    this.$on(this.m_selection, 'selectionchange', this._onSelectionChange);
    this.m_stopWidth = 8;
    this.m_stopHeight = 16;
    this.m_doubleStopCharacterWidth = 15;
    this.m_lineWidths = [];
    this.m_lineElmCount = 0;
    // 这个对象用于记录哪一行对应着第几个lineElm.
    this.m_cachedLinesIndexMap = [];
}

TextView.prototype = {
    _TPL_ELM: '<div id="#{id}" class="QEText">' +
        '<div id="#{id}_content" class="QEContent">' +
        '<div id="#{id}_measurer" style="position:absolute;left:-32768px;top:0;white-space:nowrap;"></div>' +
        '<div id="#{id}_lines" class="QELines"></div>' +
        '</div>' +
        '</div>',
    textModel: function (){
        return this.m_textModel;
    },
    selection: function (){
        return this.m_selection;
    },
    $contentElm: function (){
        return this.$elm('content');
    },
    $measurerElm: function (){
        return this.$elm('measurer');
    },
    $linesElm: function (){
        return this.$elm('lines');
    },
    scrollLeft: function (){
        return this.$elm().scrollLeft;
    },
    scrollTop: function (){
        return this.$elm().scrollTop;
    },
    clientWidth: function (){
        return this.$elm().clientWidth;
    },
    clientHeight: function (){
        return this.$elm().clientHeight;
    },
    contentWidth: function (){
        return Math.max(this.m_maxLineWidth, this.clientWidth());
    },
    contentHeight: function (){
        return Math.max(this.textModel().getLineCount() * this.m_stopHeight, this.clientHeight());
    },
    getLineHeight: function (index){
        // todo:
        return this.m_stopHeight;
    },
    firstVisibleLineIndex: function (){
        return Math.floor(this.scrollTop() / this.m_stopHeight);
    },
    lastVisibleLineIndex: function (){
        return Math.ceil((this.scrollTop() + this.clientHeight()) / this.m_stopHeight);
    },
    visibleLineCount: function (){
        return this.lastVisibleLineIndex() - this.firstVisibleLineIndex() + 1;
    },

    $calulate: function (){
        var textModel = this.textModel();
        var lineCount = textModel.getLineCount();
        var lineWidths = [];
        var maxLineWidth = 0;
        var maxLineWidthIndex = 0;
        for (var i=0; i<lineCount; i++) {
            var lineWidth = textModel.getLineEndStop(i) * this.m_stopWidth;
            if (lineWidth > maxLineWidth) {
                maxLineWidth = lineWidth;
                maxLineWidthIndex = i;
            }
            lineWidths[i] = lineWidth;
        }
        this.m_lineWidths = lineWidths;
        this.m_maxLineWidth = maxLineWidth;
        this.m_maxLineWidthIndex = maxLineWidthIndex;
        this.$contentElm().style.width = this.contentWidth() + 'px';
        this.$contentElm().style.height = this.contentHeight() + 'px';
    },
    $renderHTML: function (){
        return Utils.format(this._TPL_ELM, { id: this.$id() });
    },
    $postRender: function (){
        this._updateStopSize();
        this.m_selection.render(this.$contentElm());
        this.$on(this.$elm(), 'scroll', this._onScroll);
        this.$on(this.$contentElm(), 'mousedown', this._onMouseDown);
        this.$calulate();
        this.$redraw();
    },
    _updateStopSize: function (){
        var measurerElm = this.$measurerElm();
        measurerElm.innerHTML = Utils.stringRepeat('Wl', 1000);
        this.m_stopWidth = measurerElm.offsetWidth / 2000;
        this.m_stopHeight = measurerElm.offsetHeight;
        // todo:
        measurerElm.innerHTML = Utils.stringRepeat('国', 1000);
        this.m_doubleStopCharacterWidth = measurerElm.offsetWidth / 1000;
        measurerElm.innerHTML = '';
    },
    $updateLines: function (){
        if (!this.isRendered()) return;
        var cachedMap = this.m_cachedLinesIndexMap;
        var startLineIndex = this.firstVisibleLineIndex();
        var endLineIndex = this.lastVisibleLineIndex() + 1;
        var lineCount = endLineIndex - startLineIndex;
        var cachedCount = 0;
        var index;
        for (index in cachedMap) {
            if (index >= startLineIndex && index < endLineIndex) {
                cachedCount ++;
            }
        }
        if (cachedCount < (lineCount >> 1)) {
            this.$redraw();
            return;
        }
        // todo: 这段代码特别容易出bug.
        var linesElm = this.$linesElm();
        var fragment = [];
        fragment.startNode = linesElm.firstChild;
        var fragments = [fragment];
        var newFragmentStartNode = null;
        var i;
        var newMap = {};
        for (i=0; i<lineCount; i++) {
            index = startLineIndex + i;
            newMap[startLineIndex + i] = i;
            if (cachedMap.hasOwnProperty(index)) {
                var lineElm = linesElm.childNodes[cachedMap[index]];
                if (!fragment.endNode) {
                    fragment.endNode = lineElm;
                }
                newFragmentStartNode = lineElm.nextSibling;
            } else {
                if (fragment.endNode) {
                    fragment = [];
                    fragment.startNode = newFragmentStartNode;
                    fragments.push(fragment);
                }
                this._renderLineHTML(index, fragment);
            }
        }
        if (newFragmentStartNode && fragment.startNode !== newFragmentStartNode) {
            fragment = [];
            fragment.startNode = newFragmentStartNode;
            fragments.push(fragment);
        }
        for (i=0; i<fragments.length; i++) {
            fragment = fragments[i];
            var startNode = fragment.startNode;
            var endNode = fragment.endNode || null;
            while (startNode && startNode !== endNode) {
                var nextNode = startNode.nextSibling;
                linesElm.removeChild(startNode);
                startNode = nextNode;
            }

            if (fragment.endNode) {
                DOMUtils.insertHTML(fragment.join(''), fragment.endNode, 1);
            } else {
                DOMUtils.insertHTML(fragment.join(''), this.$linesElm(), 3);
            }
        }
        this.m_cachedLinesIndexMap = newMap;
        this.m_lineElmCount = lineCount;
        this.$linesElm().style.marginTop = (startLineIndex * this.m_stopHeight) + 'px';
    },
    $redraw: function (){
        var startLineIndex = this.firstVisibleLineIndex();
        var endLineIndex = this.lastVisibleLineIndex() + 1;
        var lineCount = endLineIndex - startLineIndex;
        var newMap = {};
        var buff = [];
        for (var i=0; i<lineCount; i++) {
            var index = startLineIndex + i;
            newMap[startLineIndex + i] = i;
            this._renderLineHTML(index, buff);
        }
        var newLinesElm = this.$linesElm().cloneNode(false);
        newLinesElm.innerHTML = buff.join('');
        this.$contentElm().replaceChild(newLinesElm, this.$linesElm());
        this.m_cachedLinesIndexMap = newMap;
        this.m_lineElmCount = lineCount;
        this.$linesElm().style.marginTop = (startLineIndex * this.m_stopHeight) + 'px';
    },
    _renderLineHTML: function (index, buff){
        // todo: 重构一下这个函数. 最好能优化一下
        var tabStop = this.textModel().tabStop();
        var letterSpacing = this.m_stopWidth * 2 - this.m_doubleStopCharacterWidth;
        var text = this.textModel().getLineText(index);
        var len = 0;
        var slotBuff;
        buff.push('<div style="height:', this.m_stopHeight, 'px;">');
        for (var i=0; i<text.length; i++) {
            var ch = text.charAt(i);
            if (_doubleStopCharacterMap[ch]) {
                if (!slotBuff) {
                    slotBuff = [];
                }
                slotBuff.push(ch);
                len += 2;
            } else {
                if (slotBuff) {
                    buff.push('<span style="letter-spacing:', letterSpacing,
                        'px;padding-left:', letterSpacing / 2, 'px;">',
                        slotBuff.join(''),
                        '</span>');
                    slotBuff = null;
                }
                if (ch == '\t') {
                    var spaces = tabStop - len % tabStop;
                    len += spaces;
                    buff.push('<span>',
                        new Array(spaces + 1).join('&#160;'), '</span>');
                } else {
                    len += 1;
                    if (ch == ' ') {
                        buff.push('&#160;');
                    } else if (ch == '<') {
                        buff.push('&lt;');
                    } else if (ch == '>') {
                        buff.push('&gt;');
                    } else if (ch == '&') {
                        buff.push('&amp;');
                    } else if (ch != '\r' && ch != '\n') {
                        buff.push(ch);
                    }
                }
            }
        }
        if (slotBuff) {
            buff.push('<span style="letter-spacing:', letterSpacing,
                'px;padding-left:', letterSpacing / 2, 'px;">',
                slotBuff.join(''),
                '</span>');
            slotBuff = null;
        }
        buff.push('</div>');
    },
    _onTextModelLinesChanged: function (evt){
        // 当textModel的行发生改变时
        // 这里需要知道哪些lineElm是需要被删掉或者更新
        // 也需要知道应该在哪个位置插入新的lineElm.
        // 这些信息都可以通过m_cachedLinesIndexMap获知.
        var lineIndex = evt.lineIndex;
        var endLineIndex = evt.endLineIndex;
        var newEndLineIndex = evt.newEndLineIndex;
        var cachedMap = this.m_cachedLinesIndexMap;
        var newMap = {};
        var damagedEndLineIndex = Math.max(newEndLineIndex, endLineIndex, lineIndex + 1);
        var index;
        var delta = newEndLineIndex - endLineIndex;
        for (index in cachedMap) {
            var lineElmIndex = cachedMap[index];
            if (index < lineIndex) {
                // 这些行是没有被影响的行, 保留它们.
                newMap[index] = lineElmIndex;
            } else if (index >= damagedEndLineIndex) {
                // 这些行是在damagedEndLineIndex之后的行, 他们的行号发生了改变, 但是仍然可用.
                newMap[index + delta] = lineElmIndex;
            }
        }
        this.m_cachedLinesIndexMap = newMap;
        // todo:延迟刷新. 优化calulate.
        this.$calulate();
        this.$updateLines();
    },
    _onSelectionChange: function (evt){
        this.dispatchEvent(evt);
    },
    hitTest: function (point, absolute){
        var textModel = this.textModel();
        if (absolute) {
            var rect = this.$contentElm().getBoundingClientRect();
            point.x -= rect.left;
            point.y -= rect.top;
        }
        var index = Math.max(0, Math.min(textModel.getLineCount(), Math.floor(point.y / this.m_stopHeight)));
        var stop = Math.max(0, Math.round(point.x / this.m_stopWidth));
        var inlineOffsetAndStop = textModel.getInlineOffsetAndStopByStop(index, stop);
        var inlineOffset = inlineOffsetAndStop[0];
        stop = inlineOffsetAndStop[1];
        var offset = textModel.getLineStartOffset(index) + inlineOffsetAndStop[0];
        return {
            offset: offset,
            lineIndex: index,
            inlineOffset: inlineOffset,
            stop: stop
        };
    },
    getPosition: function (offset){
        var textModel = this.textModel();
        var lineIndex = textModel.getLineIndex(offset);
        var inlineOffset = offset - textModel.getLineStartOffset(lineIndex);
        var stop = textModel.getStopByInlineOffset(lineIndex, inlineOffset);
        return {
            x: stop * this.m_stopWidth,
            y: lineIndex * this.m_stopHeight
        };
    },
    _onScroll: function (){
        this.$updateLines();
    },
    _onMouseDown: function (evt){
        this.dispatchEvent(evt);
    }
};

Utils.extend(TextView, Eventable);
Utils.extend(TextView, Renderable);
    </script>
</head>
<body>
<div id="d" style="width:800px;height:480px;border:2px solid #d3d3d3;"></div>
<script src="src/Utils.js"></script>
<script src="src/DOMUtils.js"></script>
<script src="src/ChunkedText_.js"></script>
<script src="src/Eventable.js"></script>
<script src="src/Renderable.js"></script>
<script src="src/Selection.js"></script>
<script src="src/TextModel.js"></script>
<script src="src/TextView.js"></script>
<script src="src/Cursor_.js"></script>
<script src="src/TextInput_.js"></script>
<script src="src/UndoManager.js"></script>
<script src="src/Editor.js"></script>
<script>
    var editor = new Editor();
    editor.render(document.getElementById('d'));
    editor.setText(document.getElementById('code').text);
</script>
</body>
</html>